package com.macasa.panel.dao.hibernate.impl;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.macasa.panel.common.log.Logger;
import com.macasa.panel.common.utils.CastHelpers;
import com.macasa.panel.dao.hibernate.api.IDao;
import com.macasa.panel.entity.api.IModel;
import com.macasa.panel.exception.DAOException;

/**
 * Class AbstractGenericHibernateDAO
 * <P>
 * 
 * @param <DTO>
 *            DTO
 * @param <ID>
 *            ID
 * @author Mickaël Husson
 * @date 01/01/2012
 */
public abstract class AbstractGenericHibernateDao<DTO extends IModel<ID>, ID extends Serializable> implements IDao<DTO, ID> {

	/** The Constant serialVersionUID. */
	private static final long			serialVersionUID	= 6332810969432234504L;

	/** The dto class. */
	private final Class<? extends DTO>	dtoClass;

	/** The id class. */
	private final Class<? extends ID>	idClass;

	@Autowired
	@Qualifier("sessionFactory")
	private SessionFactory				sessionFactory;

	/**
	 * Instantiates a new abstract generic hibernate dao.
	 */
	@SuppressWarnings("unchecked")
	protected AbstractGenericHibernateDao() {
		final ParameterizedType parameterizedType = (ParameterizedType) this.getClass().getGenericSuperclass();
		this.dtoClass = (Class<? extends DTO>) parameterizedType.getActualTypeArguments()[0];
		this.idClass = (Class<? extends ID>) parameterizedType.getActualTypeArguments()[1];
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#createOrUpdate(com.acp.salome.model.api.IDataTransfertObject)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public void createOrUpdate(final DTO entity) throws DAOException {

		try {

			this.getHibernateSession().saveOrUpdate(entity);

		} catch (final HibernateException e) {
			Logger.warn("Can't create a new entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.CREATE_UPDATE);
		}

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#delete(com.acp.salome.model.api.IDataTransfertObject)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public void delete(final DTO entity) throws DAOException {

		try {

			if (this.getHibernateSession().contains(entity)) {

				this.getHibernateSession().delete(entity);

			} else {
				Logger.warn("Can't delete entity : " + this.dtoClass.getCanonicalName() + " Not found in session ");
				throw new DAOException(new IllegalArgumentException("Can not delete entity: Not found in session "), DAOException.ERROR_CODE.DELETE);
			}

		} catch (final HibernateException e) {
			Logger.warn("Can't delete entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.DELETE);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#evict(com.acp.salome.model.api.IDataTransfertObject)
	 */
	@Override
	public void evict(final DTO dto) throws DAOException {
		try {
			this.getHibernateSession().evict(dto);
		} catch (final HibernateException e) {
			Logger.error("Can't evict entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.SESSION);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#flush()
	 */
	@Override
	public void flush() throws DAOException {
		try {
			this.getHibernateSession().flush();
		} catch (final DataAccessException e) {
			Logger.error("Can't flush entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.SESSION);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#getAllId()
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<ID> getAllId() throws DAOException {
		final String query = "select dto.id from " + this.dtoClass.getSimpleName() + " as dto ";
		try {
			return CastHelpers.castList(this.idClass, this.getHibernateSession().createQuery(query).list());

		} catch (final HibernateException e) {
			Logger.error("Can't getAllId entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * Gets the hibernate session.
	 * 
	 * @return the hibernate session
	 */
	@Override
	public Session getHibernateSession() throws DAOException {
		try {
			return this.getSessionFactory().getCurrentSession();
		} catch (final HibernateException e) {
			Logger.error("Can't getCurrentSession", e);
			throw new DAOException(e, DAOException.ERROR_CODE.SESSION);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#getfindAllNumber()
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public Integer getfindAllNumber() throws DAOException {
		try {

			final Criteria criteria = this.getEntityCriteria(this.getHibernateSession());
			this.setRowCountProjectionCriteria(criteria);

			return ((Integer) criteria.uniqueResult()).intValue();
		} catch (final HibernateException re) {
			Logger.error("Can't getfindAllNumber entity : " + this.dtoClass.getCanonicalName(), re);
			throw new DAOException(re, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findAll()
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findAll() throws DAOException {
		try {

			return CastHelpers.castList(this.dtoClass, this.getEntityCriteria(this.getHibernateSession()).list());
		} catch (final DataAccessException e) {
			Logger.error("Can't findAll entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findAll(int, int)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findAll(final int firstResult, final int maxResults) throws DAOException {
		try {
			final Criteria criteria = this.getEntityCriteria(this.getHibernateSession());

			this.setCriteriaFetchLimits(criteria, firstResult, maxResults);

			return CastHelpers.castList(this.dtoClass, criteria.list());
		} catch (final DataAccessException re) {
			Logger.error("Can't findAll entity : " + this.dtoClass.getCanonicalName(), re);
			throw new DAOException(re, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findByCriteria(org.hibernate.Criteria)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findByCriteria(final Criteria criteria) throws DAOException {
		try {
			return CastHelpers.castList(this.dtoClass, criteria.list());
		} catch (final DataAccessException e) {
			Logger.error("Can't findByCriteria entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findById(java.io.Serializable)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public DTO findById(final ID id) throws DAOException {

		try {
			return CastHelpers.cast(this.getCriteriaById(this.getHibernateSession(), id).uniqueResult(), this.dtoClass);
		} catch (final DataAccessException re) {
			Logger.error("Can't findById entity : " + this.dtoClass.getCanonicalName(), re);
			throw new DAOException(re, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findByIds(java.util.List)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findByIds(final List<ID> ids) throws DAOException {

		try {
			return CastHelpers.castList(this.dtoClass, this.getCriteriaByIdList(this.getHibernateSession(), ids).list());
		} catch (final DataAccessException re) {
			Logger.error("Can't findById entity : " + this.dtoClass.getCanonicalName(), re);
			throw new DAOException(re, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findByQuery(java.lang.String, java.lang.Object[])
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findByQuery(final String query, final Object[] paramValues) throws DAOException {
		try {
			final Query queryObject = this.getHibernateSession().createQuery(query);
			for (int i = 0; i < paramValues.length; i++) {
				queryObject.setParameter(i, paramValues[i]);
			}
			return CastHelpers.castList(this.dtoClass, queryObject.list());
		} catch (final DataAccessException e) {
			Logger.error("Can't findByQuery entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findByAttribute(java.lang.String, java.lang.Object)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findByAttribute(final String attributeName, final Object value) throws DAOException {

		try {

			final Criteria criteria = this.getCriteriaByAttribute(this.getHibernateSession(), attributeName, value);

			return CastHelpers.castList(this.dtoClass, criteria.list());

		} catch (final HibernateException e) {
			Logger.error("Can't findByAttribute entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findByAttributes(java.lang.String, java.util.List)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public List<DTO> findByAttributes(final String attributeName, final List<? extends Object> values) throws DAOException {

		try {

			final Criteria criteria = this.getCriteriaByAttributes(this.getHibernateSession(), attributeName, values);

			return CastHelpers.castList(this.dtoClass, criteria.list());

		} catch (final HibernateException e) {
			Logger.error("Can't findByAttributes entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#findByUniqueAttribute(java.lang.String, java.lang.Object)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
	public DTO findByUniqueAttribute(final String attributeName, final Object value) throws DAOException {

		try {
			if (this.getUniqueColumns().contains(attributeName)) {
				final Criteria criteria = this.getCriteriaByAttribute(this.getHibernateSession(), attributeName, value);

				return CastHelpers.cast(criteria.uniqueResult(), this.dtoClass);
			} else {
				// TODO : Modifier l'exception de DAO pour permettre d'en throws juste avec un code.
				return null;
			}
		} catch (final HibernateException e) {
			Logger.error("Can't findByUniqueAttribute entity : " + this.dtoClass.getCanonicalName(), e);
			throw new DAOException(e, DAOException.ERROR_CODE.READ);
		}
	}

	/**
	 * Create a Criteria for the current entity class
	 * 
	 * @param session
	 *            Session for creating the Criteria
	 * @return The created Criteria
	 */
	private Criteria getEntityCriteria(final Session session) {

		return session.createCriteria(this.dtoClass);
	}

	/**
	 * Create a Criteria for the current entity class with id for restriction
	 * 
	 * @param session
	 *            Session for creating the Criteria
	 * @param id
	 *            Identifier to use for restriction
	 * @return The created Criteria
	 */
	private Criteria getCriteriaById(final Session session, final ID id) {

		return this.getEntityCriteria(session).add(Restrictions.eq(this.getNameOfId(), id));
	}

	/**
	 * Create a Criteria for the current entity class with a list of id for restriction
	 * 
	 * @param session
	 *            Session for creating the Criteria
	 * @param ids
	 *            Identifier list to use for restriction
	 * @return The created Criteria
	 */
	private Criteria getCriteriaByIdList(final Session session, final List<ID> ids) {

		return this.getEntityCriteria(session).add(Restrictions.in(this.getNameOfId(), ids));
	}

	/**
	 * Create a Criteria for the current entity class with a values for a given attribute
	 * 
	 * @param session
	 *            Session for creating the Criteria
	 * @param attributeName
	 *            Attribute use to use for restriction
	 * @param value
	 *            Possible value of attribute to use for restriction
	 * @return The created Criteria
	 */
	private Criteria getCriteriaByAttribute(final Session session, final String attributeName, final Object value) {

		return this.getEntityCriteria(session).add(Restrictions.eq(attributeName, value));
	}

	/**
	 * Create a Criteria for the current entity class with a list of values for a given attribute
	 * 
	 * @param session
	 *            Session for creating the Criteria
	 * @param attributeName
	 *            Attribute use to use for restriction
	 * @param values
	 *            Possible value list of attribute to use for restriction
	 * @return The created Criteria
	 */
	private Criteria getCriteriaByAttributes(final Session session, final String attributeName, final List<?> values) {

		return this.getEntityCriteria(session).add(Restrictions.in(attributeName, values));
	}

	/**
	 * Add the possibility to count result to a given Criteria.
	 * 
	 * @param criteria
	 *            Criteria to add the possibility to count
	 */
	private void setRowCountProjectionCriteria(final Criteria criteria) {

		criteria.setProjection(Projections.rowCount());
	}

	/**
	 * Set the the first result and the max result to the given criteria
	 * 
	 * @param criteria
	 *            Criteria to limit
	 * @param limit
	 *            First number result
	 * @param offset
	 *            Max amount of result
	 */
	private void setCriteriaFetchLimits(final Criteria criteria, final int limit, final int offset) {

		if (offset > 0) {

			criteria.setFirstResult(offset);
		}

		if (limit > 0) {

			criteria.setMaxResults(limit);
		}
	}

	/**
	 * 
	 * @return
	 */
	private String getNameOfId() {

		String nameOfId = null;
		for (final Field element : this.dtoClass.getDeclaredFields()) {
			for (final Annotation annotation : element.getAnnotations()) {
				if (annotation.annotationType().getCanonicalName().equals(Id.class.getCanonicalName())
						|| annotation.annotationType().getCanonicalName().equals(EmbeddedId.class.getCanonicalName())) {
					nameOfId = element.getName();
				}
			}
		}
		return nameOfId;
	}

	/**
	 * Méthode permettant de retourner la liste des colonnes unique d'une entité
	 * 
	 * @return La liste des nom de colonnes unique
	 */
	private List<String> getUniqueColumns() {
		final List<String> names = new ArrayList<String>();
		for (final Field element : this.dtoClass.getDeclaredFields()) {
			for (final Annotation annotation : element.getAnnotations()) {
				if (annotation.annotationType().getCanonicalName().equals(Column.class.getCanonicalName()) && ((Column) annotation).unique()) {
					names.add(element.getName());
				}
			}
		}

		return names;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#getSessionFactory()
	 */
	@Override
	public final SessionFactory getSessionFactory() {
		return this.sessionFactory;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.macasa.panel.dao.hibernate.api.IDao#setSessionFactory(org.hibernate.SessionFactory)
	 */
	@Override
	public final void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * @return the dtoClass
	 */
	public final Class<? extends DTO> getDtoClass() {
		return this.dtoClass;
	}

	/**
	 * @return the idClass
	 */
	public final Class<? extends ID> getIdClass() {
		return this.idClass;
	}
}
